package org.windowkit.toolkit.widget.combo;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;

import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.plaf.basic.BasicComboPopup;
import javax.swing.plaf.basic.ComboPopup;

import com.sun.java.swing.plaf.windows.WindowsComboBoxUI;

/**
 *
 */
@SuppressWarnings("serial")
public class ComboBox extends JComboBox implements KeyListener, ItemListener, FocusListener {

	private String identifier = null;

	protected boolean inKeySelect = false;

	private boolean lastKeyNavigation = false;

	private Object lastSelected;

	SearchObject searchObject;

	ArrayList<ActionListener> actionListeners;

	public ComboBox() {
		super();
		initialize();
	}

	private void initialize() {
		setUI(new ComboboxUI());
		super.addItemListener(this);
		super.addKeyListener(this);
		addFocusListener(this);
		addKeyListener(searchObject = new SearchObject(this));
		actionListeners = new ArrayList<ActionListener>();
		lastSelected = getSelectedItem();
	}

	public ComboBox(final ComboBoxModel aModel) {
		super(aModel);
		initialize();

	}

	public void addNarrowedActionListener(final ActionListener listener) {
		if (listener != null) {
			actionListeners.add(listener);
		}
	}

	public void removeNarrowedActionListener(final ActionListener listener) {
		actionListeners.remove(listener);
	}

	/**
	 * {@inheritDoc}
	 */
	public void focusGained(final FocusEvent e) {

	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unused")
	public void focusLost(final FocusEvent e) {

	}

	/**
	 * In addtion to JComponent's processKeyEvent logic which transfers focus on
	 * TAB we catch backspace and remove any selection which exists when the key
	 * is pressed.
	 */
	@Override
    public void processKeyEvent(final KeyEvent e) {
		super.processKeyEvent(e);
		if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE && !isPopupVisible()) {
			setSelectedIndex(-1);
		}
		if (e.getKeyCode() == KeyEvent.VK_ENTER) {
			hidePopup();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public void setId(final String id) {
		identifier = id;
	}

	/**
	 * {@inheritDoc}
	 */
	public String getId() {
		return identifier;
	}

	/**
	 * {@inheritDoc}
	 */
	public void setValue(final Object values) {
		setSelectedItem(values);
	}

	public Object getValue() {
		return getModel().getSelectedItem();
	}

	@Override
    public boolean selectWithKeyChar(final char keyChar) {
		inKeySelect = true;
		// if (!isPopupVisible())
		// showPopup();
		super.selectWithKeyChar(keyChar);
		inKeySelect = false;
		return true;
	}

	/**
	 * Set the width of the text portion of this combo box.
	 *
	 * @param textWidth
	 *            The number of charachters wide to make the text portion.
	 */
	public void setTextWidth(final int textWidth) {
		String prototype = new String();
		for (int i = 0; i < textWidth; i++) {
			prototype += "B";
		}
		setPrototypeDisplayValue(prototype);
	}

	protected class ComboboxUI extends WindowsComboBoxUI {
		public ComboboxUI() {
			super();
		}

		@Override
		@SuppressWarnings("serial")
		protected ComboPopup createPopup() {
		    BasicComboPopup popup1 = new BasicComboPopup(comboBox) {
		        @Override
		        protected JScrollPane createScroller() {
		            return new JScrollPane(list,
		                    ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
		                    ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		        }

                /*
                 * Override to add more height for combos that have no vertical scroll bar
                 * but do have a horizontal scroll bar.
                 *
                 */
		        @Override
		        protected int getPopupHeightForRowCount(final int maxRowCount) {
		            int h = super.getPopupHeightForRowCount(maxRowCount);

                    if(comboBox.getItemCount() < maxRowCount){
		                //if the horizontal scrollpane is up add a little more height

		                double comboSize = comboBox.getSize().width;
		                double prefSize = scroller.getViewport().getPreferredSize().width;

		                //hopefully this test will tell us if the horizontal scroll bar will display
//		                System.out.print(prefSize + " >= " + comboSize);
		                if(prefSize >= comboSize){
		                    h += scroller.getHorizontalScrollBar().getPreferredSize().height;
		                }
		            }
		            return h;
		        }
		    };
		    return popup1;
		}

		@Override
        @SuppressWarnings("unused")
		public Dimension getPreferredSize(final JComponent c) {
			Dimension size = getDisplaySize();
			size.height += 4;
			return size;
		}
	}

	/**
	 * This class will handle the type ahead logic.
	 *
	 * @author Eric Lundin
	 */
	class SearchObject implements KeyListener {
		long lastUpdate;

		long MAX_TIME = 2000;

		StringBuilder searchCrit;

		ComboBox combo;

		public SearchObject(final ComboBox box) {
			combo = box;
			lastUpdate = Long.MIN_VALUE;
			searchCrit = new StringBuilder();
		}

		public void activateSearch(final long time) {
			if (Math.abs(time) - Math.abs(lastUpdate) > MAX_TIME) {
				if (searchCrit.length() > 0) {
					searchCrit.delete(0, searchCrit.length());
				}
			}

		}

		/**
		 * {@inheritDoc}
		 */
		public void keyTyped(final KeyEvent e) {

		}

		/**
		 * {@inheritDoc}
		 */
		public void keyPressed(final KeyEvent e) {

		}

		/*
		 * (non-Javadoc)
		 *
		 * @see java.awt.event.KeyListener#keyReleased(java.awt.event.KeyEvent)
		 */
		public void keyReleased(final KeyEvent e) {
			int keyCode = e.getKeyCode();
			char key = e.getKeyChar();
			boolean forward = true;
			if (e.isConsumed()
					|| (e.getKeyChar() == KeyEvent.CHAR_UNDEFINED && e
							.getKeyCode() != KeyEvent.VK_BACK_SPACE)
					|| e.getKeyCode() == KeyEvent.VK_ESCAPE
					|| e.getKeyChar() == '\n' || e.getKeyChar() == '\t'
					|| e.isActionKey()) {
				return;
			}
			inKeySelect = true;
			activateSearch(e.getWhen());
			if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
				handleDelete();
				key = KeyEvent.CHAR_UNDEFINED;
			}

			if (keyCode == KeyEvent.VK_BACK_SPACE
					|| keyCode == KeyEvent.VK_DELETE) {
				forward = false;
			}

			checkMatchingCharacter(key, forward);
			lastUpdate = e.getWhen();
			inKeySelect = false;
		}

		private void checkMatchingCharacter(final char currentChar, final boolean forward) {
			String currentLabel;
			String currentSearch;
			int initialIndex = Math.max(combo.getSelectedIndex(), 0);
			boolean foundMatch = false;
			if (currentChar != KeyEvent.CHAR_UNDEFINED) {
				searchCrit.append(currentChar);
			}
			currentSearch = searchCrit.toString().toUpperCase();
			if (forward) {
				for (int i = initialIndex; i < combo.getItemCount(); i++) {
					currentLabel = ((String) combo.getItemAt(i)).toUpperCase();
					if (currentLabel.startsWith(currentSearch, 0)) {
						if (i != initialIndex) {
							combo.setSelectedIndex(i);
						}
						foundMatch = true;
						break;
					}
				}
				if (!foundMatch) {
					for (int i = 0; i < initialIndex; i++) {
						currentLabel = ((String) combo.getItemAt(i));
						if (currentLabel.startsWith(searchCrit.toString(), 0)) {
							combo.setSelectedIndex(i);
							foundMatch = true;
							break;
						}
					}
				}
				if (!foundMatch) {
					searchCrit.delete(searchCrit.length() - 1, searchCrit
							.length());
				}
			} else {
				if (!foundMatch) {
					for (int i = 0; i < initialIndex; i++) {
						currentLabel = ((String) combo.getItemAt(i));
						if (currentLabel.startsWith(searchCrit.toString(), 0)) {
							combo.setSelectedIndex(i);
							foundMatch = true;
							break;
						}
					}
				}
				for (int i = initialIndex; i < combo.getItemCount(); i++) {
					currentLabel = ((String) combo.getItemAt(i)).toUpperCase();
					if (currentLabel.startsWith(currentSearch, 0)) {
						if (i != initialIndex) {
							combo.setSelectedIndex(i);
						}
						foundMatch = true;
						break;
					}
				}
			}
		}

		private void handleDelete() {
			if (searchCrit.length() > 0) {
				searchCrit.delete(searchCrit.length() - 1, searchCrit.length());
			}
		}

		public int getSearchSize() {
			return searchCrit.length();
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see java.awt.event.ItemListener#itemStateChanged(java.awt.event.ItemEvent)
	 */
	public void itemStateChanged(final ItemEvent e) {
		Object selectedItem = getSelectedItem();
		if (e.getStateChange() == ItemEvent.SELECTED
				&& !inKeySelect
				&& ((selectedItem == null && lastSelected != null) ||
				(selectedItem != null
				&& !(selectedItem.equals(lastSelected))))) {
			for (int i = 0; i < actionListeners.size(); i++) {
				actionListeners.get(i).actionPerformed(
						new ActionEvent(this, e.getID(),
								"comboboxSelectionMade"));
			}
			lastSelected = selectedItem;
		} else if (e.getStateChange() == ItemEvent.DESELECTED
				&& getModel().getSelectedItem() == null && lastSelected != null) {
			for (int i = 0; i < actionListeners.size(); i++) {
				actionListeners.get(i).actionPerformed(
						new ActionEvent(this, e.getID(),
								"comboboxSelectionMade"));
			}
			lastSelected = getSelectedItem();
		}

	}

	/**
	 * {@inheritDoc}
	 */
	public void keyTyped(final KeyEvent e) {

	}

	/**
	 * {@inheritDoc}
	 */
	public void keyPressed(final KeyEvent e) {

	}

	/**
	 * {@inheritDoc}
	 */
	public void keyReleased(final KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
			inKeySelect = true;
			setSelectedItem(lastSelected);
			inKeySelect = false;
		} else if (e.getKeyCode() == KeyEvent.VK_ENTER && !lastKeyNavigation) {
			for (int i = 0; i < actionListeners.size(); i++) {
				actionListeners.get(i).actionPerformed(
						new ActionEvent(this, e.getID(),
								"comboboxSelectionMade"));
			}
			lastSelected = getSelectedItem();
		} else if (e.getKeyCode() == KeyEvent.VK_UP
				|| e.getKeyCode() == KeyEvent.VK_DOWN
				|| e.getKeyCode() == KeyEvent.VK_LEFT
				|| e.getKeyCode() == KeyEvent.VK_RIGHT) {
			lastKeyNavigation = true;
		} else {
			lastKeyNavigation = false;
			if (e.getKeyCode() != KeyEvent.VK_BACK_SPACE
					&& !isControlCode(e.getKeyCode())) {
				showPopup();
			}
		}
	}

	protected boolean isControlCode(final int keyCode) {
		boolean returnVal;
		switch (keyCode) {
		case KeyEvent.VK_BACK_SPACE:
		case KeyEvent.VK_DELETE:
		case KeyEvent.VK_ENTER:
		case KeyEvent.VK_END:
		case KeyEvent.VK_ESCAPE:
			returnVal = true;
			break;
		default:
			returnVal = false;
		}
		return returnVal;
	}

	public void clearValue() {
		setSelectedIndex(-1);
	}
}
